在Haskell中如何将字符串转换为整数/浮点数?

80
data GroceryItem = CartItem ItemName Price Quantity | StockItem ItemName Price Quantity

makeGroceryItem :: String -> Float -> Int -> GroceryItem
makeGroceryItem name price quantity = CartItem name price quantity

I want to create a `GroceryItem` when using a `String` or `[String]`

createGroceryItem :: [String] -> GroceryItem
createGroceryItem (a:b:c) = makeGroceryItem a b c

输入的格式将会是 ["Apple","15.00","5"] ,我使用 Haskell 的 words 函数来拆分它。

我得到了以下错误,我认为这是因为 makeGroceryItem 接受一个 Float 和一个 Int

*Type error in application
*** Expression     : makeGroceryItem a read b read c
*** Term           : makeGroceryItem
*** Type           : String -> Float -> Int -> GroceryItem
*** Does not match : a -> b -> c -> d -> e -> f*

但我如何使得bc分别成为FloatInt类型?


1
你有一个有趣的项目。它是做什么的? - Martin Fischer
5个回答

100

read函数可以将字符串解析为浮点数和整数:

Prelude> :set +t
Prelude> read "123.456" :: Float
123.456
it :: Float
Prelude> read "123456" :: Int
123456
it :: Int

但问题(1)出在您的模式中:

createGroceryItem (a:b:c) = ...

:是一个右结合的二元运算符,它将一个元素添加到列表的开头。元素的右侧必须是一个列表。因此,对于表达式a:b:c,Haskell会推导出以下类型:

a :: String
b :: String
c :: [String]

即,c 将被视为字符串列表。显然它不能被 read 或传递到任何期望一个字符串的函数中。
相反,你应该使用:
createGroceryItem [a, b, c] = ...

如果列表必须恰好有3个项目,或者
createGroceryItem (a:b:c:xs) = ...

如果≥3个项目可接受。

此外(2),表达式

makeGroceryItem a read b read c

将被解释为makeGroceryItem接受5个参数,其中2个是read函数。您需要使用括号:

makeGroceryItem a (read b) (read c)

@KennyTM:read "123.456" :: Float。这个语法是什么意思?这里的::是什么?read是一个函数吗? - Nawaz
@Nawaz:是的,read 是一个函数。f :: T 表达式强制 f 具有类型 T - kennytm
@KennyTM:所以read "123.456" ::Float这个语法大致相当于C语言中的sscanf("123.456", "%f", &fnum);,对吗? - Nawaz
@Nawaz:仅适用于read函数。有关详细信息,请参见https://dev59.com/qW025IYBdhLWcg3wkW4F。 - kennytm
内部实际上Read是如何工作的?它是否将字符串分解为Char列表?然后,它是否具有从char到int的字典,并将char列表与字典进行比较以获取相应的int? - CMCDragonkai
@CMCDragonkai:它读取所有数字('0'-'9'),然后将其转换为十进制数(使用1234 = 123×10 + 4,就像其他语言一样)。实际实现相当复杂,因为它还通用支持例如读取十六进制数或读取到整数列表。如果您感兴趣,请参见https://hackage.haskell.org/package/base-4.7.0.1/docs/src/GHC-Read.html和https://hackage.haskell.org/package/base-4.7.0.1/docs/src/Text-Read-Lex.html#numberToInteger。 - kennytm

81

虽然这个问题已经有了答案,但我强烈建议使用reads进行字符串转换,因为它更加安全,不会出现无法恢复的异常情况。

reads :: (Read a) => String -> [(a, String)]

Prelude> reads "5" :: [(Double, String)]
[(5.0,"")]
Prelude> reads "5ds" :: [(Double, String)]
[(5.0,"ds")]
Prelude> reads "dffd" :: [(Double, String)]
[]

成功时,reads返回一个列表,该列表恰好包含一个元素:由转换后的值和可能无法转换的额外字符组成的元组。在失败时,reads返回一个空列表。

成功和失败都很容易进行模式匹配,并且不会使您遭受损失!


1
很好的建议!您提取从reads返回的列表中结果项的首选方法是什么?两个 fst 调用吗? - Alex
12
自 base-4.6 版本开始,在 Text.Read 模块中有 readMaybe :: Read a => String -> Maybe a 函数,使用它比在这种情况下使用 reads 更方便。 - sjakobi

5

两件事:

createGroceryItem [a, b, c] = makeGroceryItem a (parse b) (parse c)
-- pattern match error if not exactly 3 items in list

或者另一种选择是:
createGroceryItem (a : b : c : _) = makeGroceryItem a (parse b) (parse c)
-- pattern match error if fewer than 3 items in list, ignore excess items

因为:不同于++

同时,在右侧——也就是显示错误信息的一侧——你需要使用括号来分组表达式。否则,parse会被解释为你想要传递给makeGroceryItem的值,因此当你尝试向一个只接受3个参数的函数传递5个参数时,编译器会报错。


0
filterNumberFromString :: String -> String
filterNumberFromString s =
    let allowedString = ['0'..'9'] ++ ['.', ',']
        toPoint n
            | n == ',' = '.'
            | otherwise = n

        f = filter (`elem` allowedString) s
        d = map toPoint f
    in d


convertStringToFloat :: String -> Float
convertStringToFloat s =
    let betterString = filterNumberFromString s
        asFloat = read betterString :: Float
    in asFloat

print (convertStringToFloat "15,00" + 1)

-> 输出16.0

这就是我在项目中解决这个任务的方法。


0

readMaybe 可以用于此操作。与可能抛出异常的 read 不同,它也是一个完全 函数。

Prelude> import Text.Read
Prelude Text.Read> readMaybe ("1.5") :: Maybe Float
Just 1.5

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接